/*  This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 2.1 of the License, or (at
    your option) any later version.
    For more details, see the GNU Lesser General Public License (www.fsf.org
    or the COPYING file somewhere in the package)
 */

#include "Context.hh"

/*! @file src/OSGi/Context.cc
    @brief Class OSGi::Context (non-inline) methods.
    @author @ref Guillaume_Terrissol
    @date 19th December 2009 - 11th April 2014
    @note This file is distributed under the LGPL license.
    Refer to the file COPYING (or http://www.fsf.org) for more information.
 */

#include <algorithm>
#include <fstream>
#include <iostream>
#include <set>
#include <stdexcept>
#include <sstream>

#if defined(_WIN32)
#   if !defined(_WIN32_WINNT)
#       define _WIN32_WINNT 0x0502
#   endif   // !defined(_WIN32_WINNT)
#   include <windows.h>
#endif  // defined(_WIN32)

#include "ConfigFile.hh"
#include "Exception.hh"
#include "Utils.hh"

namespace OSGi
{
    /// @cond DEVELOPMENT

//------------------------------------------------------------------------------
//                        Context Private Implementation
//------------------------------------------------------------------------------

    /*! @brief Context private implementation.
        @version 0.8
        @internal
     */
    class Context::Private
    {
    public:
        //! @name Type
        //@{
        using StreamPtr = std::shared_ptr<std::ostream>;                //!< Pointer to output stream.
        //@}
                    Private(const Args&        pArgs,
                            BundleMgr*         pMgr,
                            BundleFactory::Ptr pFactory);               //!< Constructor.
        void        init(StreamPtr& pStream, const std::string& pArg);  //!< Output stream configuration.
        std::string absolute(const std::string& pPath) const;           //!< Absolute path.

        StreamPtr               mStdOut;                                //!< Output log.
        StreamPtr               mStdErr;                                //!< Error log
        BundleMgr*              mBundleMgr;                             //!< Bundle manager the context owner belongs to.
        BundleFactory::Ptr      mBundleFactory;                         //!< Bundle factory in use.
        ServiceRegistry::Ptr    mServices;                              //!< Registered services.
        Properties              mProperties;                            //!< Global properties.
        BundleList              mLoadedBundles;                         //!< Loaded bundles (may not be installed).
        BundleList              mResolvedBundles;                       //!< Resolved bundles (may be uninstalled).
        std::string             mWorkingPath;                           //!< Current absolute path.
        bool                    mIsVerbose;                             //!< Verbose status mode.
    };


//------------------------------------------------------------------------------
//                         Context Private : Constructor
//------------------------------------------------------------------------------

    /*! @param pArgs    See BundleMgr::BundleMgr(const Args&) for possible
                        values
        @param pMgr     Bundle manager this context belongs to
        @param pFactory Bundle factory in use
     */
    Context::Private::Private(const Args& pArgs, BundleMgr* pMgr, BundleFactory::Ptr pFactory)
        : mStdOut{&std::cout, [](std::ostream*) -> void { }}
        , mStdErr{&std::cerr, [](std::ostream*) -> void { }}
        , mBundleMgr{pMgr}
        , mBundleFactory{pFactory}
        , mServices{std::make_shared<ServiceRegistry>()}
        , mProperties{}
        , mLoadedBundles{}
        , mResolvedBundles{}
        , mWorkingPath{workingDirectory()}
        , mIsVerbose{false}
    {
        // Parses arguments.
        std::string lConfigFilename = absolute("osgi.ini");
        std::string lOut            = "cout";
        std::string lErr            = "cerr";
        bool        lCleanCache     = false;

        // NB: now, by default, missing files, like properties, don't generate any error.
        mProperties.set("checked",   false);
        // Default paths.
        mProperties.set("application.dir",       mWorkingPath);
        mProperties.set("application.bundleDir", absolute("bundles"));
        mProperties.set("application.cacheDir",  absolute("cache"));
        mProperties.set("application.tempDir",   absolute("tmp"));
        mProperties.set("application.permDir",   absolute("sav"));
        mProperties.set("application.configDir", absolute("cfg"));

        for(const auto& lArg : pArgs)
        {
            try
            {
                if      (lArg.find("config=") == 0)
                {
                    if (lConfigFilename.empty())    // Config file is set only once.
                    {
                        lConfigFilename = absolute(trimString(lArg.substr(sizeof("config=") - 1)));
                    }
                }
                else if (lArg == "cleanCache")
                {
                    lCleanCache = true;
                }
                else if (lArg.find("out=") == 0)
                {
                    lOut = lArg.substr(sizeof("out=") - 1);
                }
                else if (lArg.find("err=") == 0)
                {
                    lErr = lArg.substr(sizeof("err=") - 1);
                }
                else if (lArg == "checked")
                {
                    mProperties.set("checked", true);
                }
                else if (lArg == "verbose")
                {
                    mIsVerbose = true;
                }
                else
                {
                    static std::set<std::string>   sDirs = { "bundleDir", "cacheDir", "tempDir", "permDir", "configDir" };
                    for(const auto& lDir : sDirs)
                    {
                        if (lArg.find(lDir + "=") == 0)
                        {
                            // Assigns only a valid path.
                            mProperties.set("application." + lDir, absolute(trimString(lArg.substr(lDir.length() + 1))));
                        }
                    }
                }
            }
            catch(std::exception& pE)
            {
                throw std::invalid_argument{"Parsing argument " + lArg + " failed : " + pE.what()};
            }
        }

        // Initializations driven by arguments.

        // Configuration file.
        try
        {
            ConfigFile lCfg{lConfigFilename};
            lCfg.update(mProperties);
        }
        catch(std::invalid_argument&)
        {
            if (mProperties.getBool("checked"))
            {
                throw;
            }
        }

        // Logs.
        init(mStdOut, lOut);
        init(mStdErr, lErr);

        // Cache management.
        std::string lCache = mProperties.get("application.cacheDir");
        if (!lCache.empty())
        {
#if defined(_WIN32)
            SetDllDirectory(lCache.c_str());
#endif  // defined(_WIN32)
            if (lCleanCache)
            {
                emptyDirectory(mProperties.get("application.cacheDir"));
            }
        }
    }


    /*! @param pStream Output stream to initialize (either @b out, or @b err ).
        @param pArg    Allows to customize the output stream. Possibles values are :
        @li \"string\"     :@n logs are stored into a string (actually, a std::ostringstream)
        @li \"cout\"       :@n logs are sent to std::cout
        @li \"cerr\"       :@n logs are sent to std::cerr
        @li \"<b>filename</b>\":@n logs are dumped into the file @b filename
     */
    void Context::Private::init(StreamPtr& pStream, const std::string& pArg)
    {
        if      (pArg == "string")
        {
            pStream = std::make_shared<std::ostringstream>();
        }
        else if (pArg == "cout")
        {
            pStream.reset(&std::cout, [](std::ostream*) { });
        }
        else if (pArg == "cerr")
        {
            pStream.reset(&std::cerr, [](std::ostream*) { });
        }
        else if (!pArg.empty())
        {
            std::string lPath = pArg;
            if (!isPathAbsolute(lPath))
            {
                lPath = mProperties.get("application.tempDir") + "/" + lPath;  // If file isn't absolute, puts it into tempDir.
            }
            pStream = std::make_shared<std::fstream>(lPath.c_str(), std::fstream::out | std::fstream::binary);
        }
        else
        {
            throw IOError{"Invalid log"};
        }
    }


    /*! @param pPath Path to parse
        @return @p pPath, from the root
     */
    std::string Context::Private::absolute(const std::string& pPath) const
    {
        if (pPath.empty())
        {
            // Empty path is working directory.
            throw std:: invalid_argument{"Path is empty"};
        }
        else
        {
#if defined(_WIN32)
            if ((3 < pPath.length()) && std::isalpha(pPath[0]) && (pPath[1] == ':') && (pPath[2] == '/'))
#else
            if      (pPath[0] == '/')
#endif  // defined(_WIN32)
            {
                return pPath;                   // Path is absolute already.
            }
            else if (pPath == ".")
            {
                return mWorkingPath;            // "." is working directory.
            }
            else
            {
                return mWorkingPath + pPath;    // Makes absolute the relative pPath.
            }
        }
    }

    /// @endcond

//------------------------------------------------------------------------------
//                      Context : Constructor & Destructor
//------------------------------------------------------------------------------

    /*! @param pArgs    Configuration argument list
        @param pMgr     Bundle manager for which this context is defined
        @param pFactory Bundle factory used to create the bundles
     */
    Context::Context(const Args& pArgs, BundleMgr* pMgr, BundleFactory::Ptr pFactory, const Key&)
        : pthis{pArgs, pMgr, pFactory}
    { }


    /*! Defaulted.
     */
    Context::~Context() = default;


//------------------------------------------------------------------------------
//                      Context : Access to Bundles
//------------------------------------------------------------------------------

    /*! @param pName Bundle to find symbolic name
        @return The Bundle named @p pName if found, a null pointer otherwise
     */
    Bundle::Ptr Context::findBundle(const std::string& pName) const
    {
        for(const auto& lBndl : pthis->mLoadedBundles)
        {
            if (lBndl->name() == pName)
            {
                return lBndl;
            }
        }

        return {};
    }


    /*! @param pId Bundle to find id
        @return The bundle the Id of which is @p pId, if found, a null pointer
                otherwise
     */
    Bundle::Ptr Context::findBundle(int pId) const
    {
        for(const auto& lBndl : pthis->mLoadedBundles)
        {
            if (lBndl->id() == pId)
            {
                return lBndl;
            }
        }

        return {};
    }


    /*! @return A list of all loaded bundles
     */
    Context::BundleList Context::bundles() const
    {
        BundleList  lBundles;

        std::copy(begin(pthis->mLoadedBundles), end(pthis->mLoadedBundles), std::back_inserter(lBundles));

        return lBundles;
    }


    /*! @return A list of all resolved bundles
     */
    Context::BundleList Context::resolvedBundles() const
    {
        BundleList  lBundles;

        std::copy(begin(pthis->mResolvedBundles), end(pthis->mResolvedBundles), std::back_inserter(lBundles));

        return lBundles;
    }


//------------------------------------------------------------------------------
//                                  Context : Elements
//------------------------------------------------------------------------------

    /*! @return The bundle manager
     */
    BundleMgr* Context::bundleMgr() const
    {
        return pthis->mBundleMgr;
    }


    /*! @return The bundle factory
     */
    BundleFactory::Ptr Context::bundleFactory() const
    {
        return pthis->mBundleFactory;
    }


    /*! @return The service manager
     */
    ServiceRegistry::Ptr Context::services() const
    {
        return pthis->mServices;
    }


    /*! @return The properties
     */
    Properties* Context::properties() const
    {
        return &pthis->mProperties;
    }


    /*! @param pBundle Name of the bundle the properties of which are asked for
        @return The bundle @p pBundle properties if it exists, the null pointer
                otherwise
     */
    Properties* Context::properties(const std::string& pBundle) const
    {
        for(const auto& lBndl : pthis->mLoadedBundles)
        {
            if (lBndl->name() == pBundle)
            {
                return lBndl->properties();
            }
        }

        return 0;
    }


    /*! @retval true  Debug traces shall be displayed on the logs
        @retval false OSGi remains quiet, even in case of errors
     */
    bool Context::isVerbose() const
    {
        return pthis->mIsVerbose;
    }


//------------------------------------------------------------------------------
//                                  Context : Paths
//------------------------------------------------------------------------------

    /*! @return Working directory (when the BundleMgr was created)
     */
    std::string Context::applicationPath() const
    {
        return pthis->mWorkingPath;
    }


    /*! @return The path to cached data
     */
    std::string Context::cachePath() const
    {
        return pthis->mProperties.get("application.cacheDir");
    }


    /*! @return The path to persistent data
     */
    std::string Context::persistentPath() const
    {
        return pthis->mProperties.get("application.permDir");
    }


    /*! @return The path to temporary data
     */
    std::string Context::temporaryPath() const
    {
        return pthis->mProperties.get("application.tempDir");
    }


//------------------------------------------------------------------------------
//                                Context : Logs
//------------------------------------------------------------------------------

    /*! @return The standard log
     */
    std::ostream& Context::out() const
    {
        return *pthis->mStdOut;
    }


    /*! @return The error log
     */
    std::ostream& Context::err() const
    {
        return *pthis->mStdErr;
    }


//------------------------------------------------------------------------------
//                             Context : Registering
//------------------------------------------------------------------------------

    /*! @param pBundle Just insalled bundle
     */
    void Context::registerInstalledBundle(Bundle::Ptr pBundle)
    {
        pthis->mLoadedBundles.push_back(pBundle);
    }


    /*! @param pBundle Just installed bundle
     */
    void Context::registerResolvedBundle(Bundle::Ptr pBundle)
    {
        pthis->mResolvedBundles.push_back(pBundle);

        Bundle::Sorter{}(pthis->mResolvedBundles);
    }


//------------------------------------------------------------------------------
//                           Additional Documentation
//------------------------------------------------------------------------------

    /*! @page OSGi_Context_Page Execution context
        The execution context allows to access all information available in a bundle system (managed by a BundleMgr
        instance; an application may have more than once).@n
        It may be forwarded to any component which may have to access any "global" data (global according to its bundle
        manager) .@n
        @par Services
        Every registered service is accessible from the ServiceRegistry (see @ref Context::services())
        @par Bundles
        A few @ref Context_Bundles "functions" allow to retrieve the available bundles (search, or complete list).
        @par Signals
        Signals are emitted one every bundle state change (see the @ref Context_Signals "Signals" group :
        Context::bundleInstalled et al).
        @par Properties
        The global properties, as well as any bundle properties are accessible through the Context::properties()
        functions.
        @par Logs
        A context gives access to a standard log, and an error log. Their actual output (e.g. system file or string),
        is configurable (see @ref OSGi_BundleMgr_Args_Page).
        @par Paths
        The application different @ref Context_Paths "paths" (cache, temporary data,...) may be retrieved with the appropriate
        functions.
     */
}
